package com.dycong.common.IO.socket.NIO;

import lombok.extern.log4j.Log4j2;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * Created by dycong on 2017/3/23.
 */
@Log4j2
public class NIOServer {

    private static int DEFAULT_PORT = 12121;

    //todo 不设为static，不会是类的所有实例对象共享，serverSocketChannel多线程新建start时，会有端口占用异常，所以要设为static，且创建ServerSocketChannel时，同步方法，多线程访问server socket锁竞争严重
    private static ServerSocketChannel serverSocketChannel = null;
   //todo selector不必线程间共享
    private Selector selector = null;

    public NIOServer() {
    }

    public NIOServer(Integer port) {
        DEFAULT_PORT = port;
    }

    public void start() {
        start(DEFAULT_PORT);
    }

    private final void start(Integer port) {
        try {
            if (serverSocketChannel == null) {
                synchronized (NIOServer.class) {
                    if (null == serverSocketChannel) {
                        serverSocketChannel = ServerSocketChannel.open();
                        serverSocketChannel.configureBlocking(false);
                        serverSocketChannel.bind(new InetSocketAddress(DEFAULT_PORT));
                    }
                }
            }
            //todo selector不必线程间共享
            selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            //todo serverSocketChannel在register时只能选择SelectionKey.OP_ACCEPT，毕竟serverSocketChannel只是用来监听Socket
            //serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT | SelectionKey.OP_READ | SelectionKey.OP_WRITE);
            while (true) {
                //TODO select方法的返回值是指update selector维护的已注册未cancel的Channel Set,更新(新增或则说改变的)数量，本来已register的但是已select()进入Set的Channel不计数(似乎只是针对ServerSocketChannel才不计数
                // todo 对于SocketChannel即时已在Set也会计数：难道是因为ServerSocketChannel肯定是全时ready所以不计数了)，这导致ServerSocketChannel这个从不cancel的channel由于不会被select侦测到
                //TODO 从而返回0(没有新的已注册Socket ready时)，但是selectedKeys()仍然会返回ServerSocketChannel(本来就在Set,返回的只是上述Channel Set的引用)，导致下边程序不会执行，解决方法1. 不做等0判断；2. 在迭代中每一次remove当前key(迭代期里的remove会作用到集合本身),建议2

                //todo 在每次迭代时, 我们都调用 "keyIterator.remove()" 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 com.dycong.common.IO 操作放到 selectedKeys 集合中,
                //todo 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 com.dycong.common.IO 事件还在 selectedKeys 中(即时他此时不ready甚至可能已经断开).
                //todo 当然也可以将不再用的channel cancel() 这样会被加入一个cancelledKeys，select时也不会再计数(实际已被抛弃)
                int i = selector.select(500);
//                System.out.println(i);
                if (i == 0) {
                    continue;
                }
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                ServerSocketChannel server = null;
                while (it.hasNext()) {
                    SelectionKey key = (SelectionKey) it.next();
                    it.remove();
                    //todo 将这个 key 从迭代器中删除
//                    System.out.println(key.toString());
                    //// TODO: 2017/3/25 查看API：Tests whether this key's channel is ready to accept a new socket，只是判断当前ServerSocketChanner是否准备好去接收Socket，并不保障会接收到(非阻塞模式下,ACCEPT可能立即返回null)，所以要有一个判空
                    if (key.isAcceptable()) {
                        /*todo 只有ServerSocketChannel才会有isAcceptable()，Selector里包含ServerSocketChannel及SocketChannel，
                         *todo 只有在isAcceptable()时才会返回ServerSocketChannel再由其accept()到SocketChannel
                         *todo 其他情况直接得到SocketChannel*/
                        server = (ServerSocketChannel) key.channel();
                        //并不保障会接收到(非阻塞模式下)，所以要有一个判空
                        SocketChannel client = server.accept();
                        if (client == null) {
                            continue;
                        }
                        //todo important
                        client.configureBlocking(false);
                        // TODO: 2017/3/23
                        client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                    }
                    if (key.isWritable()) {
                        SocketChannel client = (SocketChannel) key.channel();
//                         TODO: 2017/3/23
//                        key.cancel(); // TODO: 2017/3/25 register的监听读写事件，此key可读可写，此处cancel()会影响下边的读，所以register时谨慎不要注册多余事件
                    }
                    if (key.isReadable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        client.read(buffer);
                        buffer.flip();
                        System.out.println(new String(buffer.array()) + "!"+Thread.currentThread());
                        buffer.clear();
                        buffer.put("hello client".getBytes());
                        buffer.flip();
                        client.write(buffer);
                        key.cancel();
                        client.close();
                    }
                }
            }
        } catch (IOException e) {
            System.out.println("server error: " + e);
        } finally {
            try {
                if (serverSocketChannel!=null&&selector!=null){
                    serverSocketChannel.close();
                    selector.close();
                }
            } catch (IOException e) {
                System.out.println("server socket channel close error: " + e);
            }
        }

    }


}
